package cc.yaomm.gateway.supply.filter;

import com.google.common.collect.Sets;
import cc.yaomm.common.basic.constant.DefaultConstant;
import cc.yaomm.common.basic.domain.KeyValue;
import cc.yaomm.common.basic.domain.Response;
import cc.yaomm.common.basic.enums.ResponseCode;
import cc.yaomm.common.basic.util.ConvertUtils;
import cc.yaomm.common.basic.util.StringsUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebExchangeDataBinder;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * Created by XQL on 2021/5/25.
 * 签名验证 过滤器 <br />
 * 核心逻辑：<br />
 * 1、判断header中是否存在appKey、timeStamp、sign、clientType、clientVersion<br />
 * 2、读取请求key-value：查询参数、form-data[排查文件参数]、x-www-form-urlencoded;读取body参数：application/json<br />
 * 3、将参数的key以ASCII码从小到大排序，拼接成字符串，再追加body内容<br />
 * 4、解析appSecret，通过客户端的appKey反向生成appSecret<br />
 * 5、然后生成签名明文字符串：接口短路径+参数字符串[第三条]+时间戳+appSecret<br />
 * 6、MD5（签名明文字符串）<br />
 * 7、对比<br />
 */
@Slf4j
public class SignFilter implements GlobalFilter, Ordered {

    private String cipher; //作用在于根据AppKey解析Secret时用到

    private boolean enabled; //是否启用签名

    public SignFilter(boolean enabled, String cipher) {
        this.enabled = enabled;
        this.cipher = cipher;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1. 获取请求
        ServerHttpRequest request = exchange.getRequest();
        //2. 获取响应
        ServerHttpResponse response = exchange.getResponse();
        //3. 判断是否验证签名
        if (!enabled) {
            return chain.filter(exchange);
        }

        //4. 获取请求头中的令牌
        HttpHeaders headers = request.getHeaders();

        try {
            //验证请求头
            //获取客户端类型
            final String clientType = headers.getFirst(DefaultConstant.HEADER_CLIENT_TYPE);
            Assert.isTrue(!StringUtils.isEmpty(clientType), "没有获取到clientType");

            final String clientVersion = headers.getFirst(DefaultConstant.HEADER_CLIENT_VERSION);
            Assert.isTrue(!StringUtils.isEmpty(clientVersion), "没有获取到clientVersion");

            //获取appKey
            final String appKey = headers.getFirst(DefaultConstant.HEADER_APP_KEY);
            Assert.isTrue(!StringUtils.isEmpty(appKey), "没有获取到appKey");

            //获取时间戳
            final Long timeStamp = ConvertUtils.toLong(headers.getFirst(DefaultConstant.HEADER_TIMESTAMP), null);
            Assert.isTrue(timeStamp != null, "没有获取到timeStamp");

            //获取签名
            final String clientSign = headers.getFirst(DefaultConstant.HEADER_SIGN);
            Assert.isTrue(!StringUtils.isEmpty(clientSign), "没有获取到sign");

            log.info("请求头信息：{}={},{}={},{}={},{}={},{}={}",
                    DefaultConstant.HEADER_CLIENT_TYPE, clientType,
                    DefaultConstant.HEADER_CLIENT_VERSION, clientVersion,
                    DefaultConstant.HEADER_APP_KEY, appKey,
                    DefaultConstant.HEADER_TIMESTAMP, timeStamp,
                    DefaultConstant.HEADER_SIGN, clientSign);

            return this.checkSign(exchange, chain, timeStamp, appKey, clientSign);
        } catch (Exception e) {
            log.error("验证签名异常", e);
            Response resp = Response.fail(ResponseCode.FAILED, e.getMessage());
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
            return response.writeWith(Flux.just(resp.toString()).map(x -> response.bufferFactory().wrap(x.getBytes())));
        }
    }

    /**
     * <p>@Description : 验证签名 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/5/27 16:42 </p>
     */
    private Mono<Void> checkSign(ServerWebExchange exchange, GatewayFilterChain chain, Long timeStamp, String appKey, String clientSign) {
        //1. 获取请求
        ServerHttpRequest request = exchange.getRequest();
        //2. 获取响应
        ServerHttpResponse response = exchange.getResponse();
        Mono<Map<String, Object>> mapMono = WebExchangeDataBinder.extractValuesToBind(exchange);
        return mapMono.flatMap(paramsMap -> {
            //构建签名明文
            StringBuilder textSb = new StringBuilder();
            //获取接口地址
            textSb.append(request.getURI().getPath());
            //获取Json Body
            String jsonBody = (String) exchange.getAttributes().get(DefaultConstant.REQUEST_BODY);
            //获取参数字符串
            textSb.append(this.getParamsStr(paramsMap, jsonBody));
            //获取时间戳
            textSb.append(timeStamp);
            //获取到appKey对应的appSecret密钥
            String appSecret = this.getAppSecret(appKey);
            textSb.append(appSecret);
            log.info("签名明文：{}", textSb);
            //生成服务器的签名
            String serverSign = DigestUtils.md5DigestAsHex(textSb.toString().getBytes()).toUpperCase();

            if (serverSign.equals(clientSign)) {
                return chain.filter(exchange);
            } else {
                Response resp = Response.fail(ResponseCode.FAILED, "签名验证失败");
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                return response.writeWith(Flux.just(resp.toString()).map(x -> response.bufferFactory().wrap(x.getBytes())));
            }
        });
    }

    /**
     * <p>@Description : 排列参数字符串</p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2018/8/2 15:54 </p>
     */
    private String getParamsStr(Map<String, Object> paramsMap, String body) {
        Set<String> keySet = Sets.newHashSet();
        paramsMap.forEach((key, value) -> {
            if (!(value instanceof Part)) { //剔除文件参数
                keySet.add(key);
            }
        });

        //key-value
        StringBuilder paramsSb = new StringBuilder();
        if (keySet.size() > 0) {
            String[] paramsStr = new String[keySet.size()];
            StringsUtils.sort(keySet.toArray(paramsStr));
            for (String key : paramsStr) {
                paramsSb.append(key);
                paramsSb.append(paramsMap.get(key));
            }
        }

        //json body
        if (StringUtils.hasText(body)) {
            paramsSb.append(body);
        }

        return paramsSb.toString();
    }

    /**
     * <p>@Description : 获取AppKey对应的Secret密钥，采用默认混淆码</p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2018/8/2 11:24 </p>
     */
    private String getAppSecret(String appKey) {
        return DigestUtils.md5DigestAsHex((appKey.concat(cipher)).getBytes()).toUpperCase();
    }

    /**
     * <p>@Description : 生成Key和密钥</p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2018/8/2 11:12 </p>
     * 生成策略：通过UUID生成大写MD5截取前8位，然后追加标示，组合成AppKey
     *
     * @param flag 安卓：AN  苹果：IO  前端：H5
     * @return key:appKey value:appSecret
     */
    private KeyValue<String, String> createAppKey(String flag) {
        byte[] uuid = UUID.randomUUID().toString().getBytes();
        String mySecret = DigestUtils.md5DigestAsHex(uuid).toUpperCase().substring(0, 8);
        KeyValue<String, String> keyValue = new KeyValue<>();
        keyValue.setKey(mySecret.concat(flag.toUpperCase()));
        String secret = DigestUtils.md5DigestAsHex((keyValue.getKey().concat(cipher)).getBytes()).toUpperCase();
        keyValue.setValue(secret);
        return keyValue;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
